/*
 * @(#)URI.java 0.3-3 06/05/2001
 *
 *  This file is part of the HTTPClient package
 *  Copyright (C) 1996-2001 Ronald Tschal�r
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free
 *  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
 *  MA 02111-1307, USA
 *
 *  For questions, suggestions, bug-reports, enhancement-requests etc.
 *  I may be contacted at:
 *
 *  ronald@innovation.ch
 *
 *  The HTTPClient's home page is located at:
 *
 *  http://www.innovation.ch/java/HTTPClient/ 
 *
 */

package org.everrest.http.client;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.BitSet;
import java.util.Hashtable;

/**
 * This class represents a generic URI, as defined in RFC-2396. This is similar
 * to java.net.URL, with the following enhancements:
 * <UL>
 * <LI>it doesn't require a URLStreamhandler to exist for the scheme; this
 * allows this class to be used to hold any URI, construct absolute URIs from
 * relative ones, etc.
 * <LI>it handles escapes correctly
 * <LI>equals() works correctly
 * <LI>relative URIs are correctly constructed
 * <LI>it has methods for accessing various fields such as userinfo, fragment,
 * params, etc.
 * <LI>it handles less common forms of resources such as the "*" used in http
 * URLs.
 * </UL>
 * <p/>
 * The elements are always stored in escaped form.
 * <p/>
 * While RFC-2396 distinguishes between just two forms of URI's, those that
 * follow the generic syntax and those that don't, this class knows about a
 * third form, named semi-generic, used by quite a few popular schemes.
 * Semi-generic syntax treats the path part as opaque, i.e. has the form
 * &lt;scheme&gt;://&lt;authority&gt;/&lt;opaque&gt; . Relative URI's of this
 * type are only resolved as far as absolute paths - relative paths do not
 * exist.
 * <p/>
 * Ideally, java.net.URL should subclass URI.
 *
 * @author Ronald Tschal�r
 * @version 0.3-3 06/05/2001
 * @see <A HREF="http://www.ics.uci.edu/pub/ietf/uri/rfc2396.txt">rfc-2396< /A>
 * @since V0.3-1
 */
public class URI {
    /**
     * If true, then the parser will resolve certain URI's in backwards
     * compatible (but technically incorrect) manner. Example:
     * <p/>
     * <PRE>
     * base   = http://a/b/c/d;p?q
     * rel    = http:g
     * result = http:g          (correct)
     * result = http://a/b/c/g  (backwards compatible)
     * </PRE>
     * <p/>
     * See rfc-2396, section 5.2, step 3, second paragraph.
     */
    public static final boolean ENABLE_BACKWARDS_COMPATIBILITY = true;

    protected static final Hashtable defaultPorts = new Hashtable();

    protected static final Hashtable usesGenericSyntax = new Hashtable();

    protected static final Hashtable usesSemiGenericSyntax = new Hashtable();

    /* various character classes as defined in the draft */
    protected static final BitSet alphanumChar;

    protected static final BitSet markChar;

    protected static final BitSet reservedChar;

    protected static final BitSet unreservedChar;

    protected static final BitSet uricChar;

    protected static final BitSet pcharChar;

    protected static final BitSet userinfoChar;

    protected static final BitSet schemeChar;

    protected static final BitSet hostChar;

    protected static final BitSet opaqueChar;

    protected static final BitSet reg_nameChar;

   /*
    * These are not directly in the spec, but used for escaping and unescaping
    * parts
    */

    /** list of characters which must not be unescaped when unescaping a scheme */
    public static final BitSet resvdSchemeChar;

    /** list of characters which must not be unescaped when unescaping a userinfo */
    public static final BitSet resvdUIChar;

    /** list of characters which must not be unescaped when unescaping a host */
    public static final BitSet resvdHostChar;

    /** list of characters which must not be unescaped when unescaping a path */
    public static final BitSet resvdPathChar;

    /**
     * list of characters which must not be unescaped when unescaping a query
     * string
     */
    public static final BitSet resvdQueryChar;

    /** list of characters which must not be escaped when escaping a path */
    public static final BitSet escpdPathChar;

    /** list of characters which must not be escaped when escaping a query string */
    public static final BitSet escpdQueryChar;

    /**
     * list of characters which must not be escaped when escaping a fragment
     * identifier
     */
    public static final BitSet escpdFragChar;

    static {
        defaultPorts.put("http", new Integer(80));
        defaultPorts.put("shttp", new Integer(80));
        defaultPorts.put("http-ng", new Integer(80));
        defaultPorts.put("coffee", new Integer(80));
        defaultPorts.put("https", new Integer(443));
        defaultPorts.put("ftp", new Integer(21));
        defaultPorts.put("telnet", new Integer(23));
        defaultPorts.put("nntp", new Integer(119));
        defaultPorts.put("news", new Integer(119));
        defaultPorts.put("snews", new Integer(563));
        defaultPorts.put("hnews", new Integer(80));
        defaultPorts.put("smtp", new Integer(25));
        defaultPorts.put("gopher", new Integer(70));
        defaultPorts.put("wais", new Integer(210));
        defaultPorts.put("whois", new Integer(43));
        defaultPorts.put("whois++", new Integer(63));
        defaultPorts.put("rwhois", new Integer(4321));
        defaultPorts.put("imap", new Integer(143));
        defaultPorts.put("pop", new Integer(110));
        defaultPorts.put("prospero", new Integer(1525));
        defaultPorts.put("irc", new Integer(194));
        defaultPorts.put("ldap", new Integer(389));
        defaultPorts.put("nfs", new Integer(2049));
        defaultPorts.put("z39.50r", new Integer(210));
        defaultPorts.put("z39.50s", new Integer(210));
        defaultPorts.put("vemmi", new Integer(575));
        defaultPorts.put("videotex", new Integer(516));
        defaultPorts.put("cmp", new Integer(829));

        usesGenericSyntax.put("http", Boolean.TRUE);
        usesGenericSyntax.put("https", Boolean.TRUE);
        usesGenericSyntax.put("shttp", Boolean.TRUE);
        usesGenericSyntax.put("coffee", Boolean.TRUE);
        usesGenericSyntax.put("ftp", Boolean.TRUE);
        usesGenericSyntax.put("file", Boolean.TRUE);
        usesGenericSyntax.put("nntp", Boolean.TRUE);
        usesGenericSyntax.put("news", Boolean.TRUE);
        usesGenericSyntax.put("snews", Boolean.TRUE);
        usesGenericSyntax.put("hnews", Boolean.TRUE);
        usesGenericSyntax.put("imap", Boolean.TRUE);
        usesGenericSyntax.put("wais", Boolean.TRUE);
        usesGenericSyntax.put("nfs", Boolean.TRUE);
        usesGenericSyntax.put("sip", Boolean.TRUE);
        usesGenericSyntax.put("sips", Boolean.TRUE);
        usesGenericSyntax.put("sipt", Boolean.TRUE);
        usesGenericSyntax.put("sipu", Boolean.TRUE);
      /*
       * Note: schemes which definitely don't use the generic-URI syntax and must
       * therefore never appear in the above list: "urn", "mailto", "sdp",
       * "service", "tv", "gsm-sms", "tel", "fax", "modem", "eid", "cid", "mid",
       * "data", "ldap"
       */

        usesSemiGenericSyntax.put("ldap", Boolean.TRUE);
        usesSemiGenericSyntax.put("irc", Boolean.TRUE);
        usesSemiGenericSyntax.put("gopher", Boolean.TRUE);
        usesSemiGenericSyntax.put("videotex", Boolean.TRUE);
        usesSemiGenericSyntax.put("rwhois", Boolean.TRUE);
        usesSemiGenericSyntax.put("whois++", Boolean.TRUE);
        usesSemiGenericSyntax.put("smtp", Boolean.TRUE);
        usesSemiGenericSyntax.put("telnet", Boolean.TRUE);
        usesSemiGenericSyntax.put("prospero", Boolean.TRUE);
        usesSemiGenericSyntax.put("pop", Boolean.TRUE);
        usesSemiGenericSyntax.put("vemmi", Boolean.TRUE);
        usesSemiGenericSyntax.put("z39.50r", Boolean.TRUE);
        usesSemiGenericSyntax.put("z39.50s", Boolean.TRUE);
        usesSemiGenericSyntax.put("stream", Boolean.TRUE);
        usesSemiGenericSyntax.put("cmp", Boolean.TRUE);

        alphanumChar = new BitSet(128);
        for (int ch = '0'; ch <= '9'; ch++)
            alphanumChar.set(ch);
        for (int ch = 'A'; ch <= 'Z'; ch++)
            alphanumChar.set(ch);
        for (int ch = 'a'; ch <= 'z'; ch++)
            alphanumChar.set(ch);

        markChar = new BitSet(128);
        markChar.set('-');
        markChar.set('_');
        markChar.set('.');
        markChar.set('!');
        markChar.set('~');
        markChar.set('*');
        markChar.set('\'');
        markChar.set('(');
        markChar.set(')');

        reservedChar = new BitSet(128);
        reservedChar.set(';');
        reservedChar.set('/');
        reservedChar.set('?');
        reservedChar.set(':');
        reservedChar.set('@');
        reservedChar.set('&');
        reservedChar.set('=');
        reservedChar.set('+');
        reservedChar.set('$');
        reservedChar.set(',');

        unreservedChar = new BitSet(128);
        unreservedChar.or(alphanumChar);
        unreservedChar.or(markChar);

        uricChar = new BitSet(128);
        uricChar.or(unreservedChar);
        uricChar.or(reservedChar);
        uricChar.set('%');

        pcharChar = new BitSet(128);
        pcharChar.or(unreservedChar);
        pcharChar.set('%');
        pcharChar.set(':');
        pcharChar.set('@');
        pcharChar.set('&');
        pcharChar.set('=');
        pcharChar.set('+');
        pcharChar.set('$');
        pcharChar.set(',');

        userinfoChar = new BitSet(128);
        userinfoChar.or(unreservedChar);
        userinfoChar.set('%');
        userinfoChar.set(';');
        userinfoChar.set(':');
        userinfoChar.set('&');
        userinfoChar.set('=');
        userinfoChar.set('+');
        userinfoChar.set('$');
        userinfoChar.set(',');

        // this actually shouldn't contain uppercase letters...
        schemeChar = new BitSet(128);
        schemeChar.or(alphanumChar);
        schemeChar.set('+');
        schemeChar.set('-');
        schemeChar.set('.');

        opaqueChar = new BitSet(128);
        opaqueChar.or(uricChar);

        hostChar = new BitSet(128);
        hostChar.or(alphanumChar);
        hostChar.set('-');
        hostChar.set('.');

        reg_nameChar = new BitSet(128);
        reg_nameChar.or(unreservedChar);
        reg_nameChar.set('$');
        reg_nameChar.set(',');
        reg_nameChar.set(';');
        reg_nameChar.set(':');
        reg_nameChar.set('@');
        reg_nameChar.set('&');
        reg_nameChar.set('=');
        reg_nameChar.set('+');

        resvdSchemeChar = new BitSet(128);
        resvdSchemeChar.set(':');

        resvdUIChar = new BitSet(128);
        resvdUIChar.set('@');

        resvdHostChar = new BitSet(128);
        resvdHostChar.set(':');
        resvdHostChar.set('/');
        resvdHostChar.set('?');
        resvdHostChar.set('#');

        resvdPathChar = new BitSet(128);
        resvdPathChar.set('/');
        resvdPathChar.set(';');
        resvdPathChar.set('?');
        resvdPathChar.set('#');

        resvdQueryChar = new BitSet(128);
        resvdQueryChar.set('#');

        escpdPathChar = new BitSet(128);
        escpdPathChar.or(pcharChar);
        escpdPathChar.set('%');
        escpdPathChar.set('/');
        escpdPathChar.set(';');

        escpdQueryChar = new BitSet(128);
        escpdQueryChar.or(uricChar);
        escpdQueryChar.clear('#');

        escpdFragChar = new BitSet(128);
        escpdFragChar.or(uricChar);
    }

   /* our uri in pieces */

    protected static final int OPAQUE = 0;

    protected static final int SEMI_GENERIC = 1;

    protected static final int GENERIC = 2;

    protected int type;

    protected String scheme;

    protected String opaque;

    protected String userinfo;

    protected String host;

    protected int port = -1;

    protected String path;

    protected String query;

    protected String fragment;

   /* cache the java.net.URL */

    protected URL url = null;

    // Constructors

    /**
     * Constructs a URI from the given string representation. The string must be
     * an absolute URI.
     *
     * @param uri
     *         a String containing an absolute URI
     * @throws ParseException
     *         if no scheme can be found or a specified port
     *         cannot be parsed as a number
     */
    public URI(String uri) throws ParseException {
        this((URI)null, uri);
    }

    /**
     * Constructs a URI from the given string representation, relative to the
     * given base URI.
     *
     * @param base
     *         the base URI, relative to which <var>rel_uri</var> is to be
     *         parsed
     * @param rel_uri
     *         a String containing a relative or absolute URI
     * @throws ParseException
     *         if <var>base</var> is null and
     *         <var>rel_uri</var> is not an absolute URI, or if
     *         <var>base</var> is not null and the scheme is not known to use
     *         the generic syntax, or if a given port cannot be parsed as a
     *         number
     */
    public URI(URI base, String rel_uri) throws ParseException {
      /*
       * Parsing is done according to the following RE:
       * ^(([^:/?#]+):)?(//([^/?#]))?([^?#])(\?([^#]))?(#(.))? 12 3 4 5 6 7 8 9 2:
       * scheme 4: authority 5: path 7: query 9: fragment
       */

        char[] uri = rel_uri.toCharArray();
        int pos = 0, idx, len = uri.length;

        // trim()

        while (pos < len && Character.isWhitespace(uri[pos]))
            pos++;
        while (len > 0 && Character.isWhitespace(uri[len - 1]))
            len--;

        // strip the special "url" or "uri" scheme

        if (pos < len - 3 && uri[pos + 3] == ':' && (uri[pos + 0] == 'u' || uri[pos + 0] == 'U')
            && (uri[pos + 1] == 'r' || uri[pos + 1] == 'R')
            && (uri[pos + 2] == 'i' || uri[pos + 2] == 'I' || uri[pos + 2] == 'l' || uri[pos + 2] == 'L'))
            pos += 4;

        // get scheme: (([^:/?#]+):)?

        idx = pos;
        while (idx < len && uri[idx] != ':' && uri[idx] != '/' && uri[idx] != '?' && uri[idx] != '#')
            idx++;
        if (idx < len && uri[idx] == ':') {
            scheme = rel_uri.substring(pos, idx).trim().toLowerCase();
            pos = idx + 1;
        }

        // check and resolve scheme

        String final_scheme = scheme;
        if (scheme == null) {
            if (base == null)
                throw new ParseException("No scheme found");
            final_scheme = base.scheme;
        }

        // check for generic vs. opaque

        type = usesGenericSyntax(final_scheme) ? GENERIC : usesSemiGenericSyntax(final_scheme) ? SEMI_GENERIC : OPAQUE;
        if (type == OPAQUE) {
            if (base != null && scheme == null)
                throw new ParseException("Can't resolve relative URI for " + "scheme " + final_scheme);

            opaque = escape(rel_uri.substring(pos), opaqueChar, true);
            if (opaque.length() > 0 && opaque.charAt(0) == '/')
                opaque = "%2F" + opaque.substring(1);
            return;
        }

        // get authority: (//([^/?#]*))?

        if (pos + 1 < len && uri[pos] == '/' && uri[pos + 1] == '/') {
            pos += 2;
            idx = pos;
            while (idx < len && uri[idx] != '/' && uri[idx] != '?' && uri[idx] != '#')
                idx++;

            parse_authority(rel_uri.substring(pos, idx), final_scheme);
            pos = idx;
        }

        // handle semi-generic and generic uri's

        if (type == SEMI_GENERIC) {
            path = escape(rel_uri.substring(pos), uricChar, true);
            if (path.length() > 0 && path.charAt(0) != '/')
                path = '/' + path;
        } else {
            // get path: ([^?#]*)

            idx = pos;
            while (idx < len && uri[idx] != '?' && uri[idx] != '#')
                idx++;
            path = escape(rel_uri.substring(pos, idx), escpdPathChar, true);
            pos = idx;

            // get query: (\?([^#]*))?

            if (pos < len && uri[pos] == '?') {
                pos += 1;
                idx = pos;
                while (idx < len && uri[idx] != '#')
                    idx++;
                this.query = escape(rel_uri.substring(pos, idx), escpdQueryChar, true);
                pos = idx;
            }

            // get fragment: (#(.*))?

            if (pos < len && uri[pos] == '#')
                this.fragment = escape(rel_uri.substring(pos + 1, len), escpdFragChar, true);
        }

        // now resolve the parts relative to the base

        if (base != null) {
            if (scheme != null && // resolve scheme
                !(scheme.equals(base.scheme) && ENABLE_BACKWARDS_COMPATIBILITY))
                return;
            scheme = base.scheme;

            if (host != null) // resolve authority
                return;
            userinfo = base.userinfo;
            host = base.host;
            port = base.port;

            if (type == SEMI_GENERIC) // can't resolve relative paths
                return;

            if (path.length() == 0 && query == null) // current doc
            {
                path = base.path;
                query = base.query;
                return;
            }

            if (path.length() == 0 || path.charAt(0) != '/') // relative path
            {
                idx = (base.path != null) ? base.path.lastIndexOf('/') : -1;
                if (idx < 0)
                    path = '/' + path;
                else
                    path = base.path.substring(0, idx + 1) + path;

                path = canonicalizePath(path);
            }
        }
    }

    /**
     * Remove all "/../" and "/./" from path, where possible. Leading "/../"'s
     * are not removed.
     *
     * @param path
     *         the path to canonicalize
     * @return the canonicalized path
     */
    public static String canonicalizePath(String path) {
        int idx, len = path.length();
        if (!((idx = path.indexOf("/.")) != -1
              && (idx == len - 2
                  || path.charAt(idx + 2) == '/'
                  || (path.charAt(idx + 2) == '.'
                      && (idx == len - 3 || path.charAt(idx + 3) == '/'))))) {
            return path;
        }

        char[] p = new char[path.length()]; // clean path
        path.getChars(0, p.length, p, 0);

        int beg = 0;
        for (idx = 1; idx < len; idx++) {
            if (p[idx] == '.' && p[idx - 1] == '/') {
                int end;
                if (idx == len - 1) // trailing "/."
                {
                    end = idx;
                    idx += 1;
                } else if (p[idx + 1] == '/') // "/./"
                {
                    end = idx - 1;
                    idx += 1;
                } else if (p[idx + 1] == '.' && (idx == len - 2 || p[idx + 2] == '/')) // "/../"
                {
                    if (idx < beg + 2) // keep from backing up too much
                    {
                        beg = idx + 2;
                        continue;
                    }

                    end = idx - 2;
                    while (end > beg && p[end] != '/')
                        end--;
                    if (p[end] != '/')
                        continue;
                    if (idx == len - 2)
                        end++;
                    idx += 2;
                } else
                    continue;
                System.arraycopy(p, idx, p, end, len - idx);
                len -= idx - end;
                idx = end;
            }
        }

        return new String(p, 0, len);
    }

    /** Parse the authority specific part */
    private void parse_authority(String authority, String scheme) throws ParseException {
      /*
       * The authority is further parsed according to:
       * ^(([^@])@?)(\[[^]]\]|[^:])?(:(.))? 12 3 4 5 2: userinfo 3: host 5: port
       */

        char[] uri = authority.toCharArray();
        int pos = 0, idx, len = uri.length;

        // get userinfo: (([^@]*)@?)

        idx = pos;
        while (idx < len && uri[idx] != '@')
            idx++;
        if (idx < len && uri[idx] == '@') {
            this.userinfo = escape(authority.substring(pos, idx), userinfoChar, true);
            pos = idx + 1;
        }

        // get host: (\[[^]]*\]|[^:]*)?

        idx = pos;
        if (idx < len && uri[idx] == '[') // IPv6
        {
            while (idx < len && uri[idx] != ']')
                idx++;
            if (idx == len)
                throw new ParseException("No closing ']' found for opening '['" + " at position " + pos + " in authority `"
                                         + authority + "'");
            this.host = authority.substring(pos + 1, idx);
            idx++;
        } else {
            while (idx < len && uri[idx] != ':')
                idx++;
            this.host = escape(authority.substring(pos, idx), uricChar, true);
        }
        pos = idx;

        // get port: (:(.*))?

        if (pos < (len - 1) && uri[pos] == ':') {
            int p;
            try {
                p = Integer.parseInt(unescape(authority.substring(pos + 1, len), null));
                if (p < 0)
                    throw new NumberFormatException();
            } catch (NumberFormatException e) {
                throw new ParseException(authority.substring(pos + 1, len) + " is an invalid port number");
            }
            if (p == defaultPort(scheme))
                this.port = -1;
            else
                this.port = p;
        }
    }

    /**
     * Construct a URI from the given URL.
     *
     * @param url
     *         the URL
     * @throws ParseException
     *         if <code>url.toExternalForm()</code> generates
     *         an invalid string representation
     */
    public URI(URL url) throws ParseException {
        this((URI)null, url.toExternalForm());
    }

    /**
     * Constructs a URI from the given parts, using the default port for this
     * scheme (if known). The parts must be in unescaped form.
     *
     * @param scheme
     *         the scheme (sometimes known as protocol)
     * @param host
     *         the host
     * @param path
     *         the path part
     * @throws ParseException
     *         if <var>scheme</var> is null
     */
    public URI(String scheme, String host, String path) throws ParseException {
        this(scheme, null, host, -1, path, null, null);
    }

    /**
     * Constructs a URI from the given parts. The parts must be in unescaped
     * form.
     *
     * @param scheme
     *         the scheme (sometimes known as protocol)
     * @param host
     *         the host
     * @param port
     *         the port
     * @param path
     *         the path part
     * @throws ParseException
     *         if <var>scheme</var> is null
     */
    public URI(String scheme, String host, int port, String path) throws ParseException {
        this(scheme, null, host, port, path, null, null);
    }

    /**
     * Constructs a URI from the given parts. Any part except for the the scheme
     * may be null. The parts must be in unescaped form.
     *
     * @param scheme
     *         the scheme (sometimes known as protocol)
     * @param userinfo
     *         the userinfo
     * @param host
     *         the host
     * @param port
     *         the port
     * @param path
     *         the path part
     * @param query
     *         the query string
     * @param fragment
     *         the fragment identifier
     * @throws ParseException
     *         if <var>scheme</var> is null
     */
    public URI(String scheme, String userinfo, String host, int port, String path, String query, String fragment)
            throws ParseException {
        if (scheme == null)
            throw new ParseException("missing scheme");
        this.scheme = escape(scheme.trim().toLowerCase(), schemeChar, true);
        if (userinfo != null)
            this.userinfo = escape(userinfo.trim(), userinfoChar, true);
        if (host != null) {
            host = host.trim();
            this.host = isIPV6Addr(host) ? host : escape(host, hostChar, true);
        }
        if (port != defaultPort(scheme))
            this.port = port;
        if (path != null)
            this.path = escape(path.trim(), escpdPathChar, true); // ???
        if (query != null)
            this.query = escape(query.trim(), escpdQueryChar, true);
        if (fragment != null)
            this.fragment = escape(fragment.trim(), escpdFragChar, true);

        type = usesGenericSyntax(scheme) ? GENERIC : SEMI_GENERIC;
    }

    private static final boolean isIPV6Addr(String host) {
        if (host.indexOf(':') < 0)
            return false;

        for (int idx = 0; idx < host.length(); idx++) {
            char ch = host.charAt(idx);
            if ((ch < '0' || ch > '9') && ch != ':')
                return false;
        }

        return true;
    }

    /**
     * Constructs an opaque URI from the given parts.
     *
     * @param scheme
     *         the scheme (sometimes known as protocol)
     * @param opaque
     *         the opaque part
     * @throws ParseException
     *         if <var>scheme</var> is null
     */
    public URI(String scheme, String opaque) throws ParseException {
        if (scheme == null)
            throw new ParseException("missing scheme");
        this.scheme = escape(scheme.trim().toLowerCase(), schemeChar, true);
        this.opaque = escape(opaque, opaqueChar, true);

        type = OPAQUE;
    }

    // Class Methods

    /**
     * @return true if the scheme should be parsed according to the generic-URI
     *         syntax
     */
    public static boolean usesGenericSyntax(String scheme) {
        return usesGenericSyntax.containsKey(scheme.trim().toLowerCase());
    }

    /**
     * @return true if the scheme should be parsed according to a
     *         semi-generic-URI syntax
     *         &lt;scheme&tgt;://&lt;hostport&gt;/&lt;opaque&gt;
     */
    public static boolean usesSemiGenericSyntax(String scheme) {
        return usesSemiGenericSyntax.containsKey(scheme.trim().toLowerCase());
    }

    /**
     * Return the default port used by a given protocol.
     *
     * @param protocol
     *         the protocol
     * @return the port number, or 0 if unknown
     */
    public final static int defaultPort(String protocol) {
        Integer port = (Integer)defaultPorts.get(protocol.trim().toLowerCase());
        return (port != null) ? port.intValue() : 0;
    }

    // Instance Methods

    /** @return the scheme (often also referred to as protocol) */
    public String getScheme() {
        return scheme;
    }

    /** @return the opaque part, or null if this URI is generic */
    public String getOpaque() {
        return opaque;
    }

    /** @return the host */
    public String getHost() {
        return host;
    }

    /** @return the port, or -1 if it's the default port, or 0 if unknown */
    public int getPort() {
        return port;
    }

    /** @return the user info */
    public String getUserinfo() {
        return userinfo;
    }

    /** @return the path */
    public String getPath() {
        return path;
    }

    /** @return the query string */
    public String getQueryString() {
        return query;
    }

    /** @return the path and query */
    public String getPathAndQuery() {
        if (query == null)
            return path;
        if (path == null)
            return "?" + query;
        return path + "?" + query;
    }

    /** @return the fragment */
    public String getFragment() {
        return fragment;
    }

    /**
     * Does the scheme specific part of this URI use the generic-URI syntax?
     * <p/>
     * In general URI are split into two categories: opaque-URI and generic-URI.
     * The generic-URI syntax is the syntax most are familiar with from URLs such
     * as ftp- and http-URLs, which is roughly:
     * <p/>
     * <PRE>
     * generic-URI = scheme &quot;:&quot; [ &quot;//&quot; server ] [ &quot;/&quot; ] [ path_segments ] [ &quot;?&quot; query ]
     * </PRE>
     * <p/>
     * (see RFC-2396 for exact syntax). Only URLs using the generic-URI syntax
     * can be used to create and resolve relative URIs.
     * <p/>
     * Whether a given scheme is parsed according to the generic-URI syntax or
     * wether it is treated as opaque is determined by an internal table of URI
     * schemes.
     *
     * @see <A HREF="http://www.ics.uci.edu/pub/ietf/uri/rfc2396.txt">rfc-2396<
     *      /A>
     */
    public boolean isGenericURI() {
        return (type == GENERIC);
    }

    /**
     * Does the scheme specific part of this URI use the semi-generic-URI syntax?
     * <p/>
     * Many schemes which don't follow the full generic syntax actually follow a
     * reduced form where the path part is treated is opaque. This is used for
     * example by ldap, smtp, pop, etc, and is roughly
     * <p/>
     * <PRE>
     * generic-URI = scheme &quot;:&quot; [ &quot;//&quot; server ] [ &quot;/&quot; [ opaque_path ] ]
     * </PRE>
     * <p/>
     * I.e. parsing is identical to the generic-syntax, except that the path part
     * is not further parsed. URLs using the semi-generic-URI syntax can be used
     * to create and resolve relative URIs with the restriction that all paths
     * are treated as absolute.
     * <p/>
     * Whether a given scheme is parsed according to the semi-generic-URI syntax
     * is determined by an internal table of URI schemes.
     *
     * @see #isGenericURI()
     */
    public boolean isSemiGenericURI() {
        return (type == SEMI_GENERIC);
    }

    /**
     * Will try to create a java.net.URL object from this URI.
     *
     * @return the URL
     * @throws MalformedURLException
     *         if no handler is available for the scheme
     */
    public URL toURL() throws MalformedURLException {
        if (url != null)
            return url;

        if (opaque != null)
            return (url = new URL(scheme + ":" + opaque));

        String hostinfo;
        if (userinfo != null && host != null)
            hostinfo = userinfo + "@" + host;
        else if (userinfo != null)
            hostinfo = userinfo + "@";
        else
            hostinfo = host;

        StringBuffer file = new StringBuffer(100);
        assemblePath(file, true, true, false);

        url = new URL(scheme, hostinfo, port, file.toString());
        return url;
    }

    private final void assemblePath(StringBuffer buf, boolean printEmpty, boolean incFragment, boolean unescape) {
        if ((path == null || path.length() == 0) && printEmpty)
            buf.append('/');

        if (path != null)
            buf.append(unescape ? unescapeNoPE(path, resvdPathChar) : path);

        if (query != null) {
            buf.append('?');
            buf.append(unescape ? unescapeNoPE(query, resvdQueryChar) : query);
        }

        if (fragment != null && incFragment) {
            buf.append('#');
            buf.append(unescape ? unescapeNoPE(fragment, null) : fragment);
        }
    }

    private final String stringify(boolean unescape) {
        StringBuffer uri = new StringBuffer(100);

        if (scheme != null) {
            uri.append(unescape ? unescapeNoPE(scheme, resvdSchemeChar) : scheme);
            uri.append(':');
        }

        if (opaque != null) // it's an opaque-uri
        {
            uri.append(unescape ? unescapeNoPE(opaque, null) : opaque);
            return uri.toString();
        }

        if (userinfo != null || host != null || port != -1)
            uri.append("//");

        if (userinfo != null) {
            uri.append(unescape ? unescapeNoPE(userinfo, resvdUIChar) : userinfo);
            uri.append('@');
        }

        if (host != null) {
            if (host.indexOf(':') < 0)
                uri.append(unescape ? unescapeNoPE(host, resvdHostChar) : host);
            else
                uri.append('[').append(host).append(']');
        }

        if (port != -1) {
            uri.append(':');
            uri.append(port);
        }

        assemblePath(uri, false, true, unescape);

        return uri.toString();
    }

    /**
     * @return a string representation of this URI suitable for use in links,
     *         headers, etc.
     */
    public String toExternalForm() {
        return stringify(false);
    }

    /**
     * Return the URI as string. This differs from toExternalForm() in that all
     * elements are unescaped before assembly. This is <em>not suitable</em> for
     * passing to other apps or in header fields and such, and is usually not
     * what you want.
     *
     * @return the URI as a string
     * @see #toExternalForm()
     */
    public String toString() {
        return stringify(true);
    }

    /**
     * @return true if <var>other</var> is either a URI or URL and it matches the
     *         current URI
     */
    public boolean equals(Object other) {
        if (other instanceof URI) {
            URI o = (URI)other;
            return (scheme.equals(o.scheme) && (type == OPAQUE && areEqual(opaque, o.opaque) ||

                                                type == SEMI_GENERIC && areEqual(userinfo, o.userinfo) && areEqualIC(host, o.host) &&
                                                port == o.port
                                                && areEqual(path, o.path) ||

                                                type == GENERIC && areEqual(userinfo, o.userinfo) && areEqualIC(host, o.host) &&
                                                port == o.port
                                                && pathsEqual(path, o.path) && areEqual(query, o.query) && areEqual(fragment, o.fragment)));
        }

        if (other instanceof URL) {
            URL o = (URL)other;
            String h, f;

            if (userinfo != null)
                h = userinfo + "@" + host;
            else
                h = host;

            f = getPathAndQuery();

            return (scheme.equalsIgnoreCase(o.getProtocol()) && (type == OPAQUE && opaque.equals(o.getFile()) ||

                                                                 type == SEMI_GENERIC && areEqualIC(h, o.getHost())
                                                                 && (port == o.getPort() || o.getPort() == defaultPort(scheme)) &&
                                                                 areEqual(f, o.getFile()) ||

                                                                 type == GENERIC && areEqualIC(h, o.getHost()) &&
                                                                 (port == o.getPort() || o.getPort() == defaultPort(scheme))
                                                                 && pathsEqual(f, o.getFile()) && areEqual(fragment, o.getRef())));
        }

        return false;
    }

    private static final boolean areEqual(String s1, String s2) {
        return (s1 == null && s2 == null || s1 != null && s2 != null
                                            && (s1.equals(s2) || unescapeNoPE(s1, null).equals(unescapeNoPE(s2, null))));
    }

    private static final boolean areEqualIC(String s1, String s2) {
        return (s1 == null && s2 == null || s1 != null && s2 != null
                                            &&
                                            (s1.equalsIgnoreCase(s2) || unescapeNoPE(s1, null).equalsIgnoreCase(unescapeNoPE(s2, null))));
    }

    private static final boolean pathsEqual(String p1, String p2) {
        if (p1 == null && p2 == null)
            return true;
        if (p1 == null || p2 == null)
            return false;
        if (p1.equals(p2))
            return true;

        // ok, so it wasn't that simple. Let's split into parts and compare
        // unescaped.
        int pos1 = 0, end1 = p1.length(), pos2 = 0, end2 = p2.length();
        while (pos1 < end1 && pos2 < end2) {
            int start1 = pos1, start2 = pos2;

            char ch;
            while (pos1 < end1 && (ch = p1.charAt(pos1)) != '/' && ch != ';')
                pos1++;
            while (pos2 < end2 && (ch = p2.charAt(pos2)) != '/' && ch != ';')
                pos2++;

            if (pos1 == end1 && pos2 < end2 || pos2 == end2 && pos1 < end1 || pos1 < end1 && pos2 < end2
                                                                              && p1.charAt(pos1) != p2.charAt(pos2))
                return false;

            if ((!p1.regionMatches(start1, p2, start2, pos1 - start1) || (pos1 - start1) != (pos2 - start2))
                && !unescapeNoPE(p1.substring(start1, pos1), null).equals(unescapeNoPE(p2.substring(start2, pos2), null)))
                return false;

            pos1++;
            pos2++;
        }

        return (pos1 == end1 && pos2 == end2);
    }

    private int hashCode = -1;

    /**
     * The hash code is calculated over scheme, host, path, and query.
     *
     * @return the hash code
     */
    public int hashCode() {
        if (hashCode == -1)
            hashCode =
                    (scheme != null ? unescapeNoPE(scheme, null).hashCode() : 0)
                    + (type == OPAQUE ? (opaque != null ? unescapeNoPE(opaque, null).hashCode() : 0) * 7 : (host != null
                                                                                                            ? unescapeNoPE(host, null)
                                                                                                                    .toLowerCase()
                                                                                                                    .hashCode() : 0)
                                                                                                           * 7
                                                                                                           + (path != null ? unescapeNoPE(
                            path, null).hashCode() : 0)
                                                                                                             * 13
                                                                                                           + (query != null ? unescapeNoPE(
                            query, null).hashCode() : 0) * 17);

        return hashCode;
    }

    /**
     * Escape any character not in the given character class. Characters greater
     * 255 are always escaped according to ??? .
     *
     * @param elem
     *         the string to escape
     * @param allowed_char
     *         the BitSet of all allowed characters
     * @param utf8
     *         if true, will first UTF-8 encode unallowed characters
     * @return the string with all characters not in allowed_char escaped
     */
    public static String escape(String elem, BitSet allowed_char, boolean utf8) {
        return new String(escape(elem.toCharArray(), allowed_char, utf8));
    }

    /**
     * Escape any character not in the given character class. Characters greater
     * 255 are always escaped according to ??? .
     *
     * @param elem
     *         the array of characters to escape
     * @param allowed_char
     *         the BitSet of all allowed characters
     * @param utf8
     *         if true, will first UTF-8 encode unallowed characters
     * @return the elem array with all characters not in allowed_char escaped
     */
    public static char[] escape(char[] elem, BitSet allowed_char, boolean utf8) {
        int cnt = 0;
        for (int idx = 0; idx < elem.length; idx++) {
            if (!allowed_char.get(elem[idx])) {
                cnt += 2;
                if (utf8) {
                    if (elem[idx] >= 0x0080)
                        cnt += 3;
                    if (elem[idx] >= 0x00800)
                        cnt += 3;
                    if ((elem[idx] & 0xFC00) == 0xD800 && idx + 1 < elem.length && (elem[idx + 1] & 0xFC00) == 0xDC00)
                        cnt -= 6;
                }
            }
        }

        if (cnt == 0)
            return elem;

        char[] tmp = new char[elem.length + cnt];
        for (int idx = 0, pos = 0; idx < elem.length; idx++) {
            char c = elem[idx];
            if (allowed_char.get(c))
                tmp[pos++] = c;
            else if (utf8) {
            /*
             * We're UTF-8 encoding the chars first, as recommended in the HTML 4.0
             * specification:
             * http://www.w3.org/TR/REC-html40/appendix/notes.html#h-B.2.1 Note that
             * this doesn't change things for ASCII chars
             */
                if (c <= 0x007F) {
                    pos = enc(tmp, pos, c);
                } else if (c <= 0x07FF) {
                    pos = enc(tmp, pos, 0xC0 | ((c >> 6) & 0x1F));
                    pos = enc(tmp, pos, 0x80 | ((c >> 0) & 0x3F));
                } else if (!((c & 0xFC00) == 0xD800 && idx + 1 < elem.length && (elem[idx + 1] & 0xFC00) == 0xDC00)) {
                    pos = enc(tmp, pos, 0xE0 | ((c >> 12) & 0x0F));
                    pos = enc(tmp, pos, 0x80 | ((c >> 6) & 0x3F));
                    pos = enc(tmp, pos, 0x80 | ((c >> 0) & 0x3F));
                } else {
                    int ch = ((c & 0x03FF) << 10) | (elem[++idx] & 0x03FF);
                    ch += 0x10000;
                    pos = enc(tmp, pos, 0xF0 | ((ch >> 18) & 0x07));
                    pos = enc(tmp, pos, 0x80 | ((ch >> 12) & 0x3F));
                    pos = enc(tmp, pos, 0x80 | ((ch >> 6) & 0x3F));
                    pos = enc(tmp, pos, 0x80 | ((ch >> 0) & 0x3F));
                }
            } else
                pos = enc(tmp, pos, c);
        }

        return tmp;
    }

    private static final char[] hex = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};

    private static final int enc(char[] out, int pos, int c) {
        out[pos++] = '%';
        out[pos++] = hex[(c >> 4) & 0xf];
        out[pos++] = hex[c & 0xf];
        return pos;
    }

    /**
     * Unescape escaped characters (i.e. %xx) except reserved ones.
     *
     * @param str
     *         the string to unescape
     * @param reserved
     *         the characters which may not be unescaped, or null
     * @return the unescaped string
     * @throws ParseException
     *         if the two digits following a `%' are not a
     *         valid hex number
     */
    public static final String unescape(String str, BitSet reserved) throws ParseException {
        if (str == null || str.indexOf('%') == -1)
            return str; // an optimization

        char[] buf = str.toCharArray();
        char[] res = new char[buf.length];

        char[] utf = new char[4];
        int utf_idx = 0, utf_len = -1;
        int didx = 0;
        for (int sidx = 0; sidx < buf.length; sidx++) {
            if (buf[sidx] == '%') {
                int ch;
                try {
                    if (sidx + 3 > buf.length)
                        throw new NumberFormatException();
                    ch = Integer.parseInt(str.substring(sidx + 1, sidx + 3), 16);
                    if (ch < 0)
                        throw new NumberFormatException();
                    sidx += 2;
                } catch (NumberFormatException e) {
               /*
                * Hmm, people not reading specs again, so we just ignore it... throw
                * new ParseException(str.substring(sidx,sidx+3) + " is an invalid
                * code");
                */
                    ch = buf[sidx];
                }

                // check if we're working on a utf-char
                if (utf_len > 0) {
                    if ((ch & 0xC0) != 0x80) // oops, we misinterpreted
                    {
                        didx = copyBuf(utf, utf_idx, ch, res, didx, reserved, false);
                        utf_len = -1;
                    } else if (utf_idx == utf_len - 1) // end-of-char
                    {
                        if ((utf[0] & 0xE0) == 0xC0)
                            ch = (utf[0] & 0x1F) << 6 | (ch & 0x3F);
                        else if ((utf[0] & 0xF0) == 0xE0)
                            ch = (utf[0] & 0x0F) << 12 | (utf[1] & 0x3F) << 6 | (ch & 0x3F);
                        else
                            ch = (utf[0] & 0x07) << 18 | (utf[1] & 0x3F) << 12 | (utf[2] & 0x3F) << 6 | (ch & 0x3F);
                        if (reserved != null && reserved.get(ch))
                            didx = copyBuf(utf, utf_idx, ch, res, didx, null, true);
                        else if (utf_len < 4)
                            res[didx++] = (char)ch;
                        else {
                            ch -= 0x10000;
                            res[didx++] = (char)((ch >> 10) | 0xD800);
                            res[didx++] = (char)((ch & 0x03FF) | 0xDC00);
                        }
                        utf_len = -1;
                    } else
                        // continue
                        utf[utf_idx++] = (char)ch;
                }
                // check if this is the start of a utf-char
                else if ((ch & 0xE0) == 0xC0 || (ch & 0xF0) == 0xE0 || (ch & 0xF8) == 0xF0) {
                    if ((ch & 0xE0) == 0xC0)
                        utf_len = 2;
                    else if ((ch & 0xF0) == 0xE0)
                        utf_len = 3;
                    else
                        utf_len = 4;
                    utf[0] = (char)ch;
                    utf_idx = 1;
                }
                // leave reserved alone
                else if (reserved != null && reserved.get(ch)) {
                    res[didx++] = buf[sidx];
                    sidx -= 2;
                }
                // just use the decoded version
                else
                    res[didx++] = (char)ch;
            } else if (utf_len > 0) // oops, we misinterpreted
            {
                didx = copyBuf(utf, utf_idx, buf[sidx], res, didx, reserved, false);
                utf_len = -1;
            } else
                res[didx++] = buf[sidx];
        }
        if (utf_len > 0) // oops, we misinterpreted
            didx = copyBuf(utf, utf_idx, -1, res, didx, reserved, false);

        return new String(res, 0, didx);
    }

    private static final int copyBuf(char[] utf, int utf_idx, int ch, char[] res, int didx, BitSet reserved,
                                     boolean escapeAll) {
        if (ch >= 0)
            utf[utf_idx++] = (char)ch;

        for (int idx = 0; idx < utf_idx; idx++) {
            if (reserved != null && reserved.get(utf[idx]) || escapeAll)
                didx = enc(res, didx, utf[idx]);
            else
                res[didx++] = utf[idx];
        }

        return didx;
    }

    /**
     * Unescape escaped characters (i.e. %xx). If a ParseException would be
     * thrown then just return the original string.
     *
     * @param str
     *         the string to unescape
     * @param reserved
     *         the characters which may not be unescaped, or null
     * @return the unescaped string, or the original string if unescaping would
     *         throw a ParseException
     * @see #unescape(java.lang.String, java.util.BitSet)
     */
    private static final String unescapeNoPE(String str, BitSet reserved) {
        try {
            return unescape(str, reserved);
        } catch (ParseException pe) {
            return str;
        }
    }

/*
   public static void main(String args[]) throws Exception
   {
      System.err.println();
      System.err.println("*** URI Tests ...");

      */
/*
       * Relative URI test set, taken from Section C of rfc-2396 and Roy's test1.
       * All Roy's URI parser tests can be found at
       * http://www.ics.uci.edu/~fielding/url/ The tests have been augmented by a
       * few for the IPv6 syntax
       *//*


      URI base = new URI("http://a/b/c/d;p?q");

      // normal examples
      testParser(base, "g:h", "g:h");
      testParser(base, "g", "http://a/b/c/g");
      testParser(base, "./g", "http://a/b/c/g");
      testParser(base, "g/", "http://a/b/c/g/");
      testParser(base, "/g", "http://a/g");
      testParser(base, "//g", "http://g");
      testParser(base, "//[23:54]", "http://[23:54]");
      testParser(base, "?y", "http://a/b/c/?y");
      testParser(base, "g?y", "http://a/b/c/g?y");
      testParser(base, "#s", "http://a/b/c/d;p?q#s");
      testParser(base, "g#s", "http://a/b/c/g#s");
      testParser(base, "g?y#s", "http://a/b/c/g?y#s");
      testParser(base, ";x", "http://a/b/c/;x");
      testParser(base, "g;x", "http://a/b/c/g;x");
      testParser(base, "g;x?y#s", "http://a/b/c/g;x?y#s");
      testParser(base, ".", "http://a/b/c/");
      testParser(base, "./", "http://a/b/c/");
      testParser(base, "..", "http://a/b/");
      testParser(base, "../", "http://a/b/");
      testParser(base, "../g", "http://a/b/g");
      testParser(base, "../..", "http://a/");
      testParser(base, "../../", "http://a/");
      testParser(base, "../../g", "http://a/g");

      // abnormal examples
      testParser(base, "", "http://a/b/c/d;p?q");
      testParser(base, "/./g", "http://a/./g");
      testParser(base, "/../g", "http://a/../g");
      testParser(base, "../../../g", "http://a/../g");
      testParser(base, "../../../../g", "http://a/../../g");
      testParser(base, "g.", "http://a/b/c/g.");
      testParser(base, ".g", "http://a/b/c/.g");
      testParser(base, "g..", "http://a/b/c/g..");
      testParser(base, "..g", "http://a/b/c/..g");
      testParser(base, "./../g", "http://a/b/g");
      testParser(base, "./g/.", "http://a/b/c/g/");
      testParser(base, "g/./h", "http://a/b/c/g/h");
      testParser(base, "g/../h", "http://a/b/c/h");
      testParser(base, "g;x=1/./y", "http://a/b/c/g;x=1/y");
      testParser(base, "g;x=1/../y", "http://a/b/c/y");
      testParser(base, "g?y/./x", "http://a/b/c/g?y/./x");
      testParser(base, "g?y/../x", "http://a/b/c/g?y/../x");
      testParser(base, "g#s/./x", "http://a/b/c/g#s/./x");
      testParser(base, "g#s/../x", "http://a/b/c/g#s/../x");
      if (ENABLE_BACKWARDS_COMPATIBILITY)
         testParser(base, "http:g", "http://a/b/c/g");
      else
         testParser(base, "http:g", "http:g");
      if (ENABLE_BACKWARDS_COMPATIBILITY)
         testParser(base, "http:", "http://a/b/c/d;p?q");
      else
         testParser(base, "http:", "http:");
      testParser(base, "./g:h", "http://a/b/c/g:h");

      */
/*
       * Roy's test2
       *//*

      base = new URI("http://a/b/c/d;p?q=1/2");

      testParser(base, "g", "http://a/b/c/g");
      testParser(base, "./g", "http://a/b/c/g");
      testParser(base, "g/", "http://a/b/c/g/");
      testParser(base, "/g", "http://a/g");
      testParser(base, "//g", "http://g");
      testParser(base, "//[23:54]", "http://[23:54]");
      testParser(base, "?y", "http://a/b/c/?y");
      testParser(base, "g?y", "http://a/b/c/g?y");
      testParser(base, "g?y/./x", "http://a/b/c/g?y/./x");
      testParser(base, "g?y/../x", "http://a/b/c/g?y/../x");
      testParser(base, "g#s", "http://a/b/c/g#s");
      testParser(base, "g#s/./x", "http://a/b/c/g#s/./x");
      testParser(base, "g#s/../x", "http://a/b/c/g#s/../x");
      testParser(base, "./", "http://a/b/c/");
      testParser(base, "../", "http://a/b/");
      testParser(base, "../g", "http://a/b/g");
      testParser(base, "../../", "http://a/");
      testParser(base, "../../g", "http://a/g");

      */
/*
       * Roy's test3
       *//*

      base = new URI("http://a/b/c/d;p=1/2?q");

      testParser(base, "g", "http://a/b/c/d;p=1/g");
      testParser(base, "./g", "http://a/b/c/d;p=1/g");
      testParser(base, "g/", "http://a/b/c/d;p=1/g/");
      testParser(base, "g?y", "http://a/b/c/d;p=1/g?y");
      testParser(base, ";x", "http://a/b/c/d;p=1/;x");
      testParser(base, "g;x", "http://a/b/c/d;p=1/g;x");
      testParser(base, "g;x=1/./y", "http://a/b/c/d;p=1/g;x=1/y");
      testParser(base, "g;x=1/../y", "http://a/b/c/d;p=1/y");
      testParser(base, "./", "http://a/b/c/d;p=1/");
      testParser(base, "../", "http://a/b/c/");
      testParser(base, "../g", "http://a/b/c/g");
      testParser(base, "../../", "http://a/b/");
      testParser(base, "../../g", "http://a/b/g");

      */
/*
       * Roy's test4
       *//*

      base = new URI("fred:///s//a/b/c");

      testParser(base, "g:h", "g:h");
      */
/*
       * we have to skip these, as usesGeneraicSyntax("fred") returns false and we
       * therefore don't parse relative URI's here. But test5 is the same except
       * that the http scheme is used. testParser(base, "g", "fred:///s//a/b/g");
       * testParser(base, "./g", "fred:///s//a/b/g"); testParser(base, "g/",
       * "fred:///s//a/b/g/"); testParser(base, "/g", "fred:///g");
       * testParser(base, "//g", "fred://g"); testParser(base, "//g/x",
       * "fred://g/x"); testParser(base, "///g", "fred:///g"); testParser(base,
       * "./", "fred:///s//a/b/"); testParser(base, "../", "fred:///s//a/");
       * testParser(base, "../g", "fred:///s//a/g"); testParser(base, "../../",
       * "fred:///s//"); testParser(base, "../../g", "fred:///s//g");
       * testParser(base, "../../../g", "fred:///s/g"); testParser(base,
       * "../../../../g", "fred:///g");
       *//*

      testPE(base, "g");

      */
/*
       * Roy's test5
       *//*

      base = new URI("http:///s//a/b/c");

      testParser(base, "g:h", "g:h");
      testParser(base, "g", "http:///s//a/b/g");
      testParser(base, "./g", "http:///s//a/b/g");
      testParser(base, "g/", "http:///s//a/b/g/");
      testParser(base, "/g", "http:///g");
      testParser(base, "//g", "http://g");
      testParser(base, "//[23:54]", "http://[23:54]");
      testParser(base, "//g/x", "http://g/x");
      testParser(base, "///g", "http:///g");
      testParser(base, "./", "http:///s//a/b/");
      testParser(base, "../", "http:///s//a/");
      testParser(base, "../g", "http:///s//a/g");
      testParser(base, "../../", "http:///s//");
      testParser(base, "../../g", "http:///s//g");
      testParser(base, "../../../g", "http:///s/g");
      testParser(base, "../../../../g", "http:///g");

      */
/*
       * Some additional parser tests
       *//*

      base = new URI("http://s");

      testParser(base, "ftp:h", "ftp:h");
      testParser(base, "ftp://h", "ftp://h");
      testParser(base, "//g", "http://g");
      testParser(base, "//g?h", "http://g?h");
      testParser(base, "g", "http://s/g");
      testParser(base, "./g", "http://s/g");
      testParser(base, "?g", "http://s/?g");
      testParser(base, "#g", "http://s#g");

      base = new URI("http:");

      testParser(base, "ftp:h", "ftp:h");
      testParser(base, "ftp://h", "ftp://h");
      testParser(base, "//g", "http://g");
      testParser(base, "g", "http:/g");
      testParser(base, "?g", "http:/?g");
      testParser(base, "#g", "http:#g");

      base = new URI("http://s/t");

      testParser(base, "ftp:/h", "ftp:/h");
      if (ENABLE_BACKWARDS_COMPATIBILITY)
         testParser(base, "http:/h", "http://s/h");
      else
         testParser(base, "http:/h", "http:/h");

      base = new URI("http://s/g?h/j");
      testParser(base, "k", "http://s/k");
      testParser(base, "k?l", "http://s/k?l");

      */
/*
       * Parser tests for semi-generic syntax
       *//*

      base = new URI("ldap:");

      testParser(base, "ldap:", "ldap:");
      testParser(base, "ldap://a", "ldap://a");
      testParser(base, "ldap://a/b", "ldap://a/b");
      testParser(base, "ldap:/b", "ldap:/b");

      testParser(base, "ftp:h", "ftp:h");
      testParser(base, "ftp://h", "ftp://h");
      testParser(base, "//g", "ldap://g");
      testParser(base, "//g?h", "ldap://g/?h");
      testParser(base, "g", "ldap:/g");
      testParser(base, "./g", "ldap:/./g");
      testParser(base, "?g", "ldap:/?g");
      testParser(base, "#g", "ldap:/%23g");

      base = new URI("ldap://s");

      if (ENABLE_BACKWARDS_COMPATIBILITY)
         testParser(base, "ldap:", "ldap://s");
      else
         testParser(base, "ldap:", "ldap:");
      testParser(base, "ldap://a", "ldap://a");
      testParser(base, "ldap://a/b", "ldap://a/b");
      if (ENABLE_BACKWARDS_COMPATIBILITY)
         testParser(base, "ldap:/b", "ldap://s/b");
      else
         testParser(base, "ldap:/b", "ldap:/b");

      testParser(base, "ftp:h", "ftp:h");
      testParser(base, "ftp://h", "ftp://h");
      testParser(base, "//g", "ldap://g");
      testParser(base, "//g?h", "ldap://g/?h");
      testParser(base, "g", "ldap://s/g");
      testParser(base, "./g", "ldap://s/./g");
      testParser(base, "?g", "ldap://s/?g");
      testParser(base, "#g", "ldap://s/%23g");

      base = new URI("ldap://s/t");

      testParser(base, "ftp:/h", "ftp:/h");
      if (ENABLE_BACKWARDS_COMPATIBILITY)
         testParser(base, "ldap:/h", "ldap://s/h");
      else
         testParser(base, "ldap:/h", "ldap:/h");

      if (ENABLE_BACKWARDS_COMPATIBILITY)
         testParser(base, "ldap:", "ldap://s");
      else
         testParser(base, "ldap:", "ldap:");
      testParser(base, "ldap://a", "ldap://a");
      testParser(base, "ldap://a/b", "ldap://a/b");

      testParser(base, "ftp:h", "ftp:h");
      testParser(base, "ftp://h", "ftp://h");
      testParser(base, "//g", "ldap://g");
      testParser(base, "//g?h", "ldap://g/?h");
      testParser(base, "g", "ldap://s/g");
      testParser(base, "./g", "ldap://s/./g");
      testParser(base, "?g", "ldap://s/?g");
      testParser(base, "#g", "ldap://s/%23g");

      */
/* equality tests *//*


      // protocol
      testNotEqual("http://a/", "nntp://a/");
      testNotEqual("http://a/", "https://a/");
      testNotEqual("http://a/", "shttp://a/");
      testEqual("http://a/", "Http://a/");
      testEqual("http://a/", "hTTP://a/");
      testEqual("url:http://a/", "hTTP://a/");
      testEqual("urI:http://a/", "hTTP://a/");

      // host
      testEqual("http://a/", "Http://A/");
      testEqual("http://a.b.c/", "Http://A.b.C/");
      testEqual("http:///", "Http:///");
      testEqual("http://[]/", "Http:///");
      testNotEqual("http:///", "Http://a/");
      testNotEqual("http://[]/", "Http://a/");
      testPE(null, "ftp://[23::43:1/");
      testPE(null, "ftp://[/");

      // port
      testEqual("http://a.b.c/", "Http://A.b.C:80/");
      testEqual("http://a.b.c:/", "Http://A.b.C:80/");
      testEqual("http://[23::45:::5:]/", "Http://[23::45:::5:]:80/");
      testEqual("http://[23::45:::5:]:/", "Http://[23::45:::5:]:80/");
      testEqual("nntp://a", "nntp://a:119");
      testEqual("nntp://a:", "nntp://a:119");
      testEqual("nntp://a/", "nntp://a:119/");
      testNotEqual("nntp://a", "nntp://a:118");
      testNotEqual("nntp://a", "nntp://a:0");
      testNotEqual("nntp://a:", "nntp://a:0");
      testEqual("telnet://:23/", "telnet:///");
      testPE(null, "ftp://:a/");
      testPE(null, "ftp://:-1/");
      testPE(null, "ftp://::1/");

      // userinfo
      testNotEqual("ftp://me@a", "ftp://a");
      testNotEqual("ftp://me@a", "ftp://Me@a");
      testEqual("ftp://Me@a", "ftp://Me@a");
      testEqual("ftp://Me:My@a:21", "ftp://Me:My@a");
      testEqual("ftp://Me:My@a:", "ftp://Me:My@a");
      testNotEqual("ftp://Me:My@a:21", "ftp://Me:my@a");
      testNotEqual("ftp://Me:My@a:", "ftp://Me:my@a");

      // path
      testEqual("ftp://a/b%2b/", "ftp://a/b+/");
      testEqual("ftp://a/b%2b/", "ftp://a/b+/");
      testEqual("ftp://a/b%5E/", "ftp://a/b^/");
      testEqual("ftp://a/b%4C/", "ftp://a/bL/");
      testNotEqual("ftp://a/b/", "ftp://a//b/");
      testNotEqual("ftp://a/b/", "ftp://a/b//");
      testNotEqual("ftp://a/b%4C/", "ftp://a/bl/");
      testNotEqual("ftp://a/b%3f/", "ftp://a/b?/");
      testNotEqual("ftp://a/b%2f/", "ftp://a/b//");
      testNotEqual("ftp://a/b%2fc/", "ftp://a/b/c/");
      testNotEqual("ftp://a/bc/", "ftp://a/b//");
      testNotEqual("ftp://a/bc/", "ftp://a/b/");
      testNotEqual("ftp://a/bc//", "ftp://a/b/");
      testNotEqual("ftp://a/b/", "ftp://a/bc//");
      testNotEqual("ftp://a/b/", "ftp://a/bc/");
      testNotEqual("ftp://a/b//", "ftp://a/bc/");

      testNotEqual("ftp://a/b;fc/", "ftp://a/bf;c/");
      testNotEqual("ftp://a/b%3bfc/", "ftp://a/b;fc/");
      testEqual("ftp://a/b;/;/", "ftp://a/b;/;/");
      testNotEqual("ftp://a/b;/", "ftp://a/b//");
      testNotEqual("ftp://a/b//", "ftp://a/b;/");
      testNotEqual("ftp://a/b/;", "ftp://a/b//");
      testNotEqual("ftp://a/b//", "ftp://a/b/;");
      testNotEqual("ftp://a/b;/", "ftp://a/b;//");
      testNotEqual("ftp://a/b;//", "ftp://a/b;/");

      // escaping/unescaping
      testEscape("hello\u1212there", "hello%E1%88%92there");
      testEscape("hello\u0232there", "hello%C8%B2there");
      testEscape("hello\uDA42\uDD42there", "hello%F2%A0%A5%82there");
      testEscape("hello\uDA42", "hello%ED%A9%82");
      testEscape("hello\uDA42there", "hello%ED%A9%82there");
      testUnescape("hello%F2%A0%A5%82there", "hello\uDA42\uDD42there");
      testUnescape("hello%F2%A0%A5there", "hello\u00F2\u00A0\u00A5there");
      testUnescape("hello%F2%A0there", "hello\u00F2\u00A0there");
      testUnescape("hello%F2there", "hello\u00F2there");
      testUnescape("hello%F2%A0%A5%82", "hello\uDA42\uDD42");
      testUnescape("hello%F2%A0%A5", "hello\u00F2\u00A0\u00A5");
      testUnescape("hello%F2%A0", "hello\u00F2\u00A0");
      testUnescape("hello%F2", "hello\u00F2");
      testUnescape("hello%E1%88%92there", "hello\u1212there");
      testUnescape("hello%E1%88there", "hello\u00E1\u0088there");
      testUnescape("hello%E1there", "hello\u00E1there");
      testUnescape("hello%E1%71there", "hello\u00E1qthere");
      testUnescape("hello%E1%88", "hello\u00E1\u0088");
      testUnescape("hello%E1%71", "hello\u00E1q");
      testUnescape("hello%E1", "hello\u00E1");
      testUnescape("hello%C8%B2there", "hello\u0232there");
      testUnescape("hello%C8there", "hello\u00C8there");
      testUnescape("hello%C8%71there", "hello\u00C8qthere");
      testUnescape("hello%C8%71", "hello\u00C8q");
      testUnescape("hello%C8", "hello\u00C8");
      testUnescape("%71there", "qthere");
      testUnescape("%B1there", "\u00B1there");

      System.err.println("*** Tests finished successfuly");
   }
*/

    private static final String nl = System.getProperty("line.separator");

/*
   private static void testParser(URI base, String relURI, String result) throws Exception
   {
      if (!(new URI(base, relURI).toExternalForm().equals(result)))
      {
         throw new Exception("Test failed: " + nl + "  base-URI = <" + base + ">" + nl + "  rel-URI  = <" + relURI
            + ">" + nl + "  expected   <" + result + ">" + nl + "  but got    <" + new URI(base, relURI) + ">");
      }
   }

   private static void testEqual(String one, String two) throws Exception
   {
      URI u1 = new URI(one);
      URI u2 = new URI(two);

      if (!u1.equals(u2))
      {
         throw new Exception("Test failed: " + nl + "  <" + one + "> != <" + two + ">");
      }
      if (u1.hashCode() != u2.hashCode())
      {
         throw new Exception("Test failed: " + nl + "  hashCode <" + one + "> != hashCode <" + two + ">");
      }
   }

   private static void testNotEqual(String one, String two) throws Exception
   {
      URI u1 = new URI(one);
      URI u2 = new URI(two);

      if (u1.equals(u2))
      {
         throw new Exception("Test failed: " + nl + "  <" + one + "> == <" + two + ">");
      }
   }

   private static void testPE(URI base, String uri) throws Exception
   {
      boolean got_pe = false;
      try
      {
         new URI(base, uri);
      }
      catch (ParseException pe)
      {
         got_pe = true;
      }
      if (!got_pe)
      {
         throw new Exception("Test failed: " + nl + "  <" + uri + "> should be invalid");
      }
   }

   private static void testEscape(String raw, String escaped) throws Exception
   {
      String test = new String(escape(raw.toCharArray(), uricChar, true));
      if (!test.equals(escaped))
         throw new Exception("Test failed: " + nl + "  raw-string: " + raw + nl + "  escaped:    " + test + nl
            + "  expected:   " + escaped);
   }

   private static void testUnescape(String escaped, String raw) throws Exception
   {
      if (!unescape(escaped, null).equals(raw))
         throw new Exception("Test failed: " + nl + "  escaped-string: " + escaped + nl + "  unescaped:      "
            + unescape(escaped, null) + nl + "  expected:       " + raw);
   }
*/
}
